前言

昨天复现了 metinfo 6.2.0 的一个前台任意文件上传的漏洞,发现了一些其他利用方式~

代码审计

为了复现该漏洞,所以搭建环境如下。

windows + php 5.3.29

安装完该 cms 后,我先从前台首页跟了一遍代码的执行流程,大致熟悉了一下路由的规则。

index.php

1
2
3
4
5
define('M_NAME', 'index');
define('M_MODULE', 'web');
define('M_CLASS', 'index');
define('M_ACTION', 'doindex');
require_once './app/system/entrance.php';

跟进 /app/system/entrance.php 。

01

文件最后调用了 load::module(),其中调用了 self::_load_class() 去调用相应类的相应方法。

1
2
3
4
5
6
7
8
9
public static function module($path = '', $modulename = '', $action = '') {
if (!$path) {
if (!$path) $path = PATH_OWN_FILE;
if (!$modulename) $modulename = M_CLASS;
if (!$action) $action = M_ACTION;
if (!$action) $action = 'doindex';
}
return self::_load_class($path, $modulename, $action);
}

因为漏洞发生在 uploadify 类的 doupfile 方法中,找了一下发现可以路由到该方法的文件为 admin/index.php。在该文件开头设置了一个常量。

1
define('IN_ADMIN', true);

该常量和 gpc 中数据的过滤逻辑相关。在 metinfo 中有一个全局变量 $_M,其中的 form 键对应的值为 gpc 中的数据。

02

因为设置了常量 IN_ADMIN 的值,所以这里不会对 gpc 中数据调用 sqlinsert 函数进行处理,这样就不会把\转换成/了,这和后面的参考文章的 payload 有关。

接着回到 uploadify 类中,因为 uploadify 类继承自 web 类,其构造方法中调用了 web 类的构造方法,web 类是一个前台基类,所以并没有做权限相关的验证。根据上面的分析,已经可以路由到触发漏洞的 doupfile 方法中。

在 /app/system/include/module/uploadify.class.php。

03

这里 savepath 和 is_rename 都是 gpc 中的数据,我们可控,跟进 upload 方法。

1
2
3
4
5
public function upload($formname){
global $_M;
$back = $this->upfile->upload($formname);
return $back;
}

其中调用了 /app/system/include/class/upfile.class.php 中的 upload 方法。

04

我把需要注意的地方圈起来了,自己跟一遍,大致就能发现构造 payload 的难点,第一个是文件后缀名的限制,第二个是目录名的限制。

先说一下文件后缀名的限制,文件名在上传前经过了 iconv 函数的处理,而文件名是 savepath 和 savename 属性拼接来的,这两个属性都是我们可控的。而 iconv 这个函数在 php < 5.4 中是存在截断问题的。

iconv 截断:https://www.cnblogs.com/milantgh/p/3602141.html

所以我们可以构造类似a.php%80.jpg这样的文件名,绕过文件后缀名的限制,但是因为在前面的 set_savename 函数中,会将 savename 属性中最后一个点.之前的多余的点.替换成_,所以截断的 payload 需要放在 savepath 属性中,在最后和 savename 拼接文件名的时候形成类似a.php%80.jpg这样的文件名,然后进入 iconv 函数,截断后变成a.php,从而绕过后缀名的检测。

所以我们的重点就放在了 savepath 属性上。在一开始触发漏洞的 doupfile 函数中,设置了 savepath 属性的值。

05

实际上,savepath 属性的值是PATH_WEB.'/upload'.xx.'/',最后的/是在 path_standard 函数中加上的,其中 xx 是我们传参中 savepath 对应的值。这里要注意就是 savepath 结尾字符一定是/

回到 upload 方法,可以知道 savepath 属性需要满足以下要求。

  • PATH_WEB. 'upload/'开头(已满足)
  • 不含./
  • 进入 makedir 函数返回 true

跟进 makedir 函数。

06

这里的逻辑就是会递归创建 savepath 指定的目录,如果成功则返回 true。

我们重新理一下整个思路:

  • 路由到 uploadify 类的 doupfile 方法中。
  • 上传文件名的后缀在白名单中,如a.jpg,该值存放在 savename 属性中;将截断 payload 放在 savepath 参数中,如shell.php%80,在给 savepath 属性赋值的时候会拼接绝对路径,此时 savepath 属性的值为PATH_WEB.'upload/'.'shell.php%80'.'/'
  • savepath 属性和 savename 属性拼接成文件名 file_name,即webroot/upload/shell.php%80/a.jpg,进入 iconv 函数,截断后变成webroot/upload/shell.php,成功上传文件至 upload 目录。

这里唯一需要注意的,就是 savepath 属性在进入 makedir 函数创建目录时,因为其目录名中存在特殊字符,所以可能会因为创建失败而返回 false。

windows 目录名与 gbk

当使用 %80 在 iconv 函数中截断后缀时,%80 经过 url 解码成特殊字符。

07

此时进入 makedir 函数的 savepath 属性值为webroot/upload/shell.php�/(savepath 属性值结尾拼接了/,我们实际传入的是shell.php%80

08

在 windows 的中文系统中,文件名的默认编码为 gbk,在 linux 中默认编码为 utf-8,这里%80对应的 gbk 字符可以作为 windows 下的目录名。

gbk 编码:https://www.23bei.com/tool-54.html#

09

所以该目录名合法,这样就通过了 makedir 的检测,成功上传了文件。

10

上传的 shell.php 文件因为 iconv 截断,所以是在 upload 目录下,而 makedir 创建的目录只是为了验证 savepath 属性指定的路径可创建,只要 makedir 能创建目录返回 true,就能到 iconv 截断处从而上传文件。

参考文章中

在 iconv 转换字符集时,如果字符串中存在源字符集序列不允许的字符时会造成截断。utf-8 在单字节时允许的范围为 0x00-0x7F。

所以实际上大于等于 %80 的字符都会造成 iconv 函数的截断,而测试发现,除了 %80 以外的单字节字符都无法成为 windows 下的合法目录名,所以在 makedir 创建目录时,就会返回 false,这样就到不了 iconv 截断处了。

因为 windows 下目录分割符可以是\,所以参考文章中使用..\来实现目录穿越,这样就会在 file_exists 函数检测时判断目录存在,就不会进入 mkdir 中创建目录。这里需要注意的是,一开始我们说了 gpc 中的数据不会经过 sqlinsert 函数处理,所以\不会替换成/,这样在进入 makedir 函数前对 savepath 检测时,就不会检测到./。将 get 中的 savepath 参数设置为shell.php%81/..\,发现 file_exists 检测为 false,且 mkdir 无法创建该名字的目录,这就导致了 makedir 返回 false。

15

测试后发现shell.php%81\..\可以通过目录跳转使 file_exists 函数返回 true,即 makedir 返回 true。

16

那还有什么办法让 %80 以外的能造成 iconv 截断的单字节字符,能通过 makedir 函数对目录名的检测呢?

回想一下,sql 中的宽字节注入。当数据库编码是 gbk 且单引号被转义时,在单引号前面输入 %df,即1%df\',这样进入数据库时,单字节%df和反斜线对应的单字节%5c,组成双字节df5c,即,这样单引号就成功逃逸了。

11

同理,因为 windows 中目录名以 gbk 编码,所以假如我们使用任意大于等于 %80 的其他字符触发 iconv 截断,如 %88,而在 makedir 创建目录时,%88 对应的字符不合法。但因为 gbk 编码范围为 8140 - FEFE,所以我们可以在后面拼接一个单字节,和 %88 组成双字节后成为合法 gbk 字符,这样就可以创建 savepath 指定的目录了。

12

这样就通过了 makedir 的检测,并成功上传了文件。

13

可以看一下885c对应的 gbk 字符,和目录名中的字符是一样的。

14

在这个场景中,上传文件所在的目录与 makedir 创建的目录不是一个目录,所以 gbk 字符并不影响我们使用 webshell,但是如果 shell 文件在 makedir 创建的目录下的话,因为目录名中带有 gbk 字符,所以是不能直接访问到的。

17

总结

该漏洞的触发需要在 windows 条件下,且 php < 5.4,这样才能利用 iconv 截断的特性去控制文件后缀,还有就是 savepath 指定的 windows 目录名要合法,可以通过\..\实现目录穿越绕过 file_exists 函数的检测,或者可以通过组合 gbk 字符使目录可创建。

ref :

https://xz.aliyun.com/t/5603

http://www.yulegeyu.com/2019/06/18/Metinfo6-Arbitrary-File-Upload-Via-Iconv-Truncate/

https://www.qqxiuzi.cn/zh/hanzi-gbk-bianma.php

https://www.cnblogs.com/milantgh/p/3602141.html

https://www.23bei.com/tool-54.html#